/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain (a) copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kizitonwose.calendarview;

import com.kizitonwose.calendarview.bean.ViewConfig;
import com.kizitonwose.calendarview.model.CalendarDay;
import com.kizitonwose.calendarview.model.CalendarMonth;
import com.kizitonwose.calendarview.model.MonthConfig;
import com.kizitonwose.calendarview.model.enums.DayOwner;
import com.kizitonwose.calendarview.model.enums.InDateStyle;
import com.kizitonwose.calendarview.model.enums.OutDateStyle;
import com.kizitonwose.calendarview.model.enums.ScrollMode;
import com.kizitonwose.calendarview.ui.CalendarAdapter;
import com.kizitonwose.calendarview.ui.CalendarLayoutManager;
import com.kizitonwose.calendarview.uinterface.DayBinder;
import com.kizitonwose.calendarview.uinterface.MonthHeaderFooterBinder;
import com.kizitonwose.calendarview.uinterface.MonthScrollListener;
import com.kizitonwose.calendarview.utils.AttrUtils;
import com.kizitonwose.calendarview.utils.DiffUtil;
import com.kizitonwose.calendarview.utils.Size;
import com.kizitonwose.calendarview.utils.Utils;

import ohos.agp.components.AttrSet;
import ohos.agp.components.BaseItemProvider;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.ComponentTreeObserver;
import ohos.agp.components.ListContainer;
import ohos.agp.components.ScrollHelper;
import ohos.app.Context;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.List;

/**
 * CalendarView
 *
 * @since 2021-02-26
 */
public class CalendarView extends ListContainer implements Component.EstimateSizeListener,
    ListContainer.ItemVisibilityChangedListener, ComponentTreeObserver.ScrollChangedListener {
    private static final HiLogLabel LABEL = new HiLogLabel(HiLog.DEBUG, 0, "CalendarView");
    private static final int SQUARE = Integer.MIN_VALUE;
    private static final Size SIZE_SQUARE = new Size(Integer.MIN_VALUE, Integer.MIN_VALUE);
    /**
     * The [DayBinder] instance used for managing day cell views
     * creation and reuse. Changing the binder means that the view
     * creation logic could have changed too. We refresh the Calender.
     */
    private DayBinder<?> dayBinder = null;

    /**
     * The [MonthHeaderFooterBinder] instance used for managing header views.
     * The header view is shown above each month on the Calendar.
     */
    private MonthHeaderFooterBinder<?> monthHeaderBinder = null;

    /**
     * The [MonthHeaderFooterBinder] instance used for managing footer views.
     * The footer view is shown below each month on the Calendar.
     */
    private MonthHeaderFooterBinder<?> monthFooterBinder = null;
    /**
     * Called when the calender scrolls to (a) new month. Mostly beneficial
     * if [ScrollMode] is [ScrollMode.PAGED].
     */
    private MonthScrollListener monthScrollListener = null;
    /**
     * The xml resource that is inflated and used as the day cell view.
     * This must be provided.
     */
    private int dayViewResource = 0;
    /**
     * The xml resource that is inflated and used as (a) header for every month.
     * Set zero to disable.
     */
    private int monthHeaderResource = 0;
    /**
     * The xml resource that is inflated and used as (a) footer for every month.
     * Set zero to disable.
     */
    private int monthFooterResource;

    /**
     * A [ViewGroup] which is instantiated and used as the background for each month.
     * This class must have (a) constructor which takes only (a) [Context]. You should
     * exclude the name and constructor of this class from code obfuscation if enabled.
     */
    private String monthViewClass;
    /**
     * The [RecyclerView.Orientation] used for the layout manager.
     * This determines the scroll direction of the the calendar.
     */
    private int mOrientation = VERTICAL;

    /**
     * The scrolling behavior of the calendar. If [ScrollMode.PAGED],
     * the calendar will snap to the nearest month after (a) scroll or swipe action.
     * If [ScrollMode.CONTINUOUS], the calendar scrolls normally.
     */
    private ScrollMode scrollMode = ScrollMode.CONTINUOUS;
    /**
     * Determines how inDates are generated for each month on the calendar.
     * If set to [InDateStyle.ALL_MONTHS], inDates will be generated for all months.
     * If set to [InDateStyle.FIRST_MONTH], inDates will be generated for the first month only.
     * If set to [InDateStyle.NONE], inDates will not be generated, this means there will
     * be no offset on any month.
     * <p>
     * Note: This causes calendar data to be regenerated, consider using [updateMonthConfiguration]
     * if updating this property alongside [outDateStyle], [maxRowCount] or [hasBoundaries].
     */
    private InDateStyle inDateStyle = InDateStyle.ALL_MONTHS;
    /**
     * Determines how outDates are generated for each month on the calendar.
     * If set to [OutDateStyle.END_OF_ROW], the calendar will generate outDates until
     * it reaches the first end of (a) row. This means that if (a) month has 6 rows,
     * it will display 6 rows and if (a) month has 5 rows, it will display 5 rows.
     * If set to [OutDateStyle.END_OF_GRID], the calendar will generate outDates until
     * it reaches the end of (a) 6 x 7 grid. This means that all months will have 6 rows.
     * If set to [OutDateStyle.NONE], no outDates will be generated.
     * <p>
     * Note: This causes calendar data to be regenerated, consider using [updateMonthConfiguration]
     * if updating this value property [inDateStyle], [maxRowCount] or [hasBoundaries].
     */
    private OutDateStyle outDateStyle = OutDateStyle.END_OF_ROW;
    /**
     * The maximum number of rows(1 to 6) to show on each month. If (a) month has (a) total of 6
     * rows and [maxRowCount] is set to 4, there will be two appearances of that month on the,
     * calendar the first one will show 4 rows and the second one will show the remaining 2 rows.
     * To show (a) week mode calendar, set this value to 1.
     * <p>
     * Note: This causes calendar data to be regenerated, consider using [updateMonthConfiguration]
     * if updating this property alongside [inDateStyle], [outDateStyle] or [hasBoundaries].
     */
    private int maxRowCount = 6;
    /**
     * Determines if dates of (a) month should stay in its section or can flow into another month's section.
     * If true, (a) section can only contain dates belonging to that month, its inDates and outDates.
     * if false, the dates are added continuously, irrespective of month sections.
     * <p>
     * When this property is false, (a) few things behave slightly differently:
     * - If [InDateStyle] is either [InDateStyle.ALL_MONTHS] or [InDateStyle.FIRST_MONTH], only the first index
     * will contain inDates.
     * - If [OutDateStyle] is either [OutDateStyle.END_OF_ROW] or [OutDateStyle.END_OF_GRID],
     * only the last index will contain outDates.
     * - If [OutDateStyle] is [OutDateStyle.END_OF_GRID], outDates are generated for the last index until it
     * satisfies the [maxRowCount] requirement.
     * <p>
     * Note: This causes calendar data to be regenerated, consider using [updateMonthConfiguration]
     * if updating this property alongside [inDateStyle], [outDateStyle] or [maxRowCount].
     */
    private boolean hasBoundaries = true;

    /**
     * The duration in milliseconds of the animation used to adjust the CalendarView's
     * height when [scrollMode] is [ScrollMode.PAGED] and the CalendarView's height is
     * set to `wrap_content`. The height change happens when the CalendarView scrolls to
     * (a) month which has less or more rows than the previous one. Default value is 200.
     * To disable the animation, set this value to zero.
     */

    private int wrappedPageHeightAnimationDuration = 200;

    private YearMonth startMonth;
    private YearMonth endMonth;
    private DayOfWeek firstDayOfWeek;
    private boolean autoSize = true;
    private int autoSizeHeight = SQUARE;
    private boolean sizedInternally = false;
    private boolean internalConfigUpdate = false;
    private ScrollHelper scrollHelper;

    /**
     * The size in pixels for each day cell view.
     * Set this to [SIZE_SQUARE] to have (a) nice
     * square item view.
     *
     * @see [SIZE_SQUARE]
     */
    private Size daySize = SIZE_SQUARE;

    private int monthPaddingStart;

    private int monthPaddingEnd;

    private int monthPaddingTop;

    private int monthPaddingBottom;

    private int monthMarginStart;

    private int monthMarginEnd;

    private int monthMarginTop;

    private int monthMarginBottom;

    // todo 用于监听列表的滚动状态  停止滚动时进行操作
    private ScrolledListener scrollListenerInternal;

    private CalendarAdapter calendarAdapter;
    private CalendarLayoutManager calendarLayoutManager;

    /**
     * CalendarView
     *
     * @param context Context
     */
    public CalendarView(Context context) {
        this(context, null);
    }

    /**
     * CalendarView
     *
     * @param context Context
     * @param attrSet AttrSet
     */
    public CalendarView(Context context, AttrSet attrSet) {
        this(context, attrSet, null);
        HiLog.info(LABEL, "初始化开始01：");
    }

    /**
     * CalendarView
     *
     * @param context Context
     * @param attrSet AttrSet
     * @param styleName String
     */
    public CalendarView(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        HiLog.info(LABEL, "初始化开始01：");
        init(context, attrSet, styleName);
    }

    public CalendarLayoutManager getCalendarLayoutManager() {
        return calendarLayoutManager;
    }

    public CalendarAdapter getCalendarAdapter() {
        return (CalendarAdapter) getItemProvider();
    }

    private void init(Context context, AttrSet attrSet, String styleName) {
        HiLog.info(LABEL, "初始化开始01：");
        initAttr(context, attrSet);

        scrollListenerInternal = new ScrolledListener() {
            @Override
            public void onContentScrolled(Component component, int i, int i1, int i2, int i3) {
            }

            @Override
            public void scrolledStageUpdate(Component component, int newStage) {
            }
        };
    }

    private void initAttr(Context context, AttrSet attrSet) {
        scrollHelper = new ScrollHelper();
        getComponentTreeObserver().addScrolledListener(this);

        setDayViewResource(AttrUtils.getResourceId(attrSet, "cv_dayViewResource"));
        setMonthHeaderResource(AttrUtils.getResourceId(attrSet, "cv_monthHeaderResource"));
        setMonthFooterResource(AttrUtils.getResourceId(attrSet, "cv_monthFooterResource"));

        if (attrSet.getAttr("cv_orientation").isPresent()) {
            setMyOrientation(attrSet.getAttr("cv_orientation").get().getIntegerValue());
        }
        if (attrSet.getAttr("cv_scrollMode").isPresent()) {
            scrollMode = ScrollMode.values()[attrSet.getAttr("cv_scrollMode").get().getIntegerValue()];
        }
        if (attrSet.getAttr("cv_outDateStyle").isPresent()) {
            setOutDateStyle(OutDateStyle.values()[attrSet.getAttr("cv_outDateStyle").get().getIntegerValue()]);
        }
        if (attrSet.getAttr("cv_inDateStyle").isPresent()) {
            setInDateStyle(InDateStyle.values()[attrSet.getAttr("cv_inDateStyle").get().getIntegerValue()]);
        }
        if (attrSet.getAttr("cv_maxRowCount").isPresent()) {
            setMaxRowCount(attrSet.getAttr("cv_maxRowCount").get().getIntegerValue());
        }
        if (attrSet.getAttr("cv_monthViewClass").isPresent()) {
            setMonthViewClass(attrSet.getAttr("cv_monthViewClass").get().getStringValue());
        }
        if (attrSet.getAttr("cv_hasBoundaries").isPresent()) {
            setHasBoundaries(attrSet.getAttr("cv_hasBoundaries").get().getBoolValue());
        }
        if (attrSet.getAttr("cv_wrappedPageHeightAnimationDuration").isPresent()) {
            wrappedPageHeightAnimationDuration = attrSet.getAttr("cv_wrappedPageHeightAnimationDuration").get().getIntegerValue();
        }

        HiLog.info(LABEL, "初始化开始01：" + dayViewResource);

        if (dayViewResource == 0) {
            throw new IllegalStateException("No value set for `cv_dayViewResource` attribute.");
        }

        setEstimateSizeListener(this);
    }

    /**
     * Setup the CalendarView.
     * See [updateMonthRange] and [updateMonthRangeAsync] to change
     * the [startMonth] and [endMonth] values.
     *
     * @param startMonth The first month on the calendar.
     * @param endMonth The last month on the calendar.
     * @param firstDayOfWeek An instance of [DayOfWeek] enum to be the first day of week.
     */
    public final void setup(YearMonth startMonth, YearMonth endMonth, DayOfWeek firstDayOfWeek) {
        this.startMonth = startMonth;
        this.endMonth = endMonth;
        this.firstDayOfWeek = firstDayOfWeek;
        this.finishSetup(new MonthConfig(this.outDateStyle, this.inDateStyle, this.maxRowCount,
            startMonth, endMonth, firstDayOfWeek, this.hasBoundaries));
    }

    public boolean isAutoSize() {
        return autoSize;
    }

    /**
     * Setup the CalendarView, asynchronously.
     * Useful if your [startMonth] and [endMonth] values are many years apart.
     * See [updateMonthRange] and [updateMonthRangeAsync] to change the
     * [startMonth] and [endMonth] values.
     * <p>
     * Note: the setup MUST finish before any other methods can are called. To be
     * notified when the setup is finished, provide (a) [completion] parameter.
     *
     * @param startMonth The first month on the calendar.
     * @param endMonth The last month on the calendar.
     * @param firstDayOfWeek An instance of [DayOfWeek] enum to be the first day of week.
     */
    public final void setupAsync(final YearMonth startMonth, final YearMonth endMonth, final DayOfWeek firstDayOfWeek) {
        this.startMonth = startMonth;
        this.endMonth = endMonth;
        this.firstDayOfWeek = firstDayOfWeek;
        MonthConfig monthConfig = new MonthConfig(
            outDateStyle, inDateStyle, maxRowCount, startMonth,
            endMonth, firstDayOfWeek, hasBoundaries
        );
        getContext().getUITaskDispatcher().asyncDispatch(() -> {
            finishSetup(monthConfig);
        });
    }

    private void finishSetup(MonthConfig monthConfig) {
        setScrolledListener(scrollListenerInternal);
        calendarLayoutManager = new CalendarLayoutManager(this);
        setItemProvider(new CalendarAdapter(this, new ViewConfig(this.dayViewResource, this.monthHeaderResource,
            this.monthFooterResource, this.monthViewClass), monthConfig, getContext()));
    }

    /**
     * Update the CalendarView's start and end months.
     * This can be called only if you have called [setup] or [setupAsync] in the past.
     * See [updateMonthRangeAsync] if you wish to do this asynchronously.
     */
    public final void updateMonthRange() {
        updateMonthRange(requireStartMonth(), requireEndMonth());
    }

    /**
     * updateMonthRange
     *
     * @param startMonth YearMonth
     * @param endMonth YearMonth
     */
    public final void updateMonthRange(YearMonth startMonth, YearMonth endMonth) {
        this.startMonth = startMonth;
        this.endMonth = endMonth;

        MonthConfig monthConfig = generateMonthConfig();
        finishUpdateMonthRange(monthConfig, null);
    }

    /**
     * Update the CalendarView's start and end months, asynchronously.
     * This can be called only if you have called [setup] or [setupAsync] in the past.
     * Useful if your [startMonth] and [endMonth] values are many years apart.
     * See [updateMonthRange] if you wish to do this synchronously.
     */
    public final void updateMonthRangeAsync() {
        updateMonthRangeAsync(requireStartMonth(), requireEndMonth());
    }

    /**
     * updateMonthRangeAsync
     *
     * @param startMonth YearMonth
     * @param endMonth YearMonth
     */
    public final void updateMonthRangeAsync(YearMonth startMonth, YearMonth endMonth) {
        this.startMonth = startMonth;
        this.endMonth = endMonth;
        MonthConfig monthConfig = generateMonthConfig();
        getContext().getUITaskDispatcher().asyncDispatch(() -> {
            finishUpdateMonthRange(monthConfig, null);
        });
    }

    private void finishUpdateMonthRange(MonthConfig newConfig, DiffUtil.DiffResult diffResult) {
        calendarAdapter.setMonthConfig(newConfig);
        calendarAdapter.notifyDataChanged();
    }

    @Override
    public void setItemProvider(BaseItemProvider itemProvider) {
        super.setItemProvider(itemProvider);
        this.calendarAdapter = (CalendarAdapter) itemProvider;
    }

    @Override
    public boolean isBoundToWindow() {
        return super.isBoundToWindow();
    }

    private MonthConfig generateMonthConfig() {
        return new MonthConfig(
            outDateStyle,
            inDateStyle,
            maxRowCount,
            requireStartMonth(),
            requireEndMonth(),
            requireFirstDayOfWeek(),
            hasBoundaries
        );
    }

    private YearMonth requireStartMonth() {
        if (startMonth == null) {
            throw new IllegalStateException("`startMonth` is not set. Have you called `setup()`?");
        } else {
            return startMonth;
        }
    }

    private YearMonth requireEndMonth() {
        if (endMonth == null) {
            throw new IllegalStateException("`endMonth` is not set. Have you called `setup()`?");
        } else {
            return endMonth;
        }
    }

    private DayOfWeek requireFirstDayOfWeek() {
        if (firstDayOfWeek == null) {
            throw new IllegalStateException("`firstDayOfWeek` is not set. Have you called `setup()`?");
        } else {
            return firstDayOfWeek;
        }
    }

    /**
     * 判断是否是Horizontal
     *
     * @return isHorizontal boolean
     */
    public boolean isHorizontal() {
        return !isVertical();
    }

    /**
     * 判断是否是Vertical
     *
     * @return isVertical boolean
     */
    public boolean isVertical() {
        return mOrientation == VERTICAL;
    }

    public final Size getDaySize() {
        return this.daySize;
    }

    /**
     * setDaySize
     *
     * @param value Size
     */
    public final void setDaySize(Size value) {
        this.daySize = value;
        if (!this.sizedInternally) {
            this.autoSize = value == null
                || (value.getWidth() == SIZE_SQUARE.getWidth()
                && value.getHeight() == SIZE_SQUARE.getHeight())
                || value.getWidth() == SQUARE;
            if (value != null) {
                this.autoSizeHeight = value.getHeight();
            }
            this.invalidateViewHolders();
        }
    }

    public final ScrollMode getScrollMode() {
        return this.scrollMode;
    }

    /**
     * setScrollMode
     *
     * @param value ScrollMode
     */
    public final void setScrollMode(ScrollMode value) {
        if (this.scrollMode != value) {
            this.scrollMode = value;
        }
    }

    public final InDateStyle getInDateStyle() {
        return this.inDateStyle;
    }

    /**
     * setInDateStyle
     *
     * @param value InDateStyle
     */
    public final void setInDateStyle(InDateStyle value) {
        if (this.inDateStyle != value) {
            this.inDateStyle = value;
            updateAdapterMonthConfig();
        }
    }

    public final OutDateStyle getOutDateStyle() {
        return this.outDateStyle;
    }

    /**
     * setOutDateStyle
     *
     * @param value OutDateStyle
     */
    public final void setOutDateStyle(OutDateStyle value) {
        if (this.outDateStyle != value) {
            this.outDateStyle = value;
            updateAdapterMonthConfig();
        }
    }

    public final int getMaxRowCount() {
        return this.maxRowCount;
    }

    /**
     * setMaxRowCount
     *
     * @param value int
     * @throws IllegalArgumentException
     */
    public final void setMaxRowCount(int value) {
        if (value < 1 || value > 7) {
            throw new IllegalArgumentException("'maxRowCount' should be between 1 to 6");
        } else {
            if (this.maxRowCount != value) {
                this.maxRowCount = value;
                updateAdapterMonthConfig();
            }
        }
    }

    public final boolean getHasBoundaries() {
        return this.hasBoundaries;
    }

    /**
     * setHasBoundaries
     *
     * @param isBoundaries boolean
     */
    public final void setHasBoundaries(boolean isBoundaries) {
        if (this.hasBoundaries != isBoundaries) {
            this.hasBoundaries = isBoundaries;
            updateAdapterMonthConfig();
        }
    }

    public final int getOrientation() {
        return this.mOrientation;
    }

    /**
     * setMyOrientation
     *
     * @param value int
     */
    public final void setMyOrientation(int value) {
        if (this.mOrientation != value) {
            this.mOrientation = value;
            setOrientation(mOrientation);
            YearMonth mStartMonth = this.startMonth;
            if (mStartMonth == null) {
                return;
            }

            YearMonth mEndMonth = this.endMonth;
            if (mEndMonth == null) {
                return;
            }

            DayOfWeek mFirstDayOfWeek = this.firstDayOfWeek;
            if (mFirstDayOfWeek == null) {
                return;
            }

            setup(mStartMonth, mEndMonth, mFirstDayOfWeek);
        }
    }

    public final String getMonthViewClass() {
        return this.monthViewClass;
    }

    /**
     * setMonthViewClass
     *
     * @param value int
     */
    public final void setMonthViewClass(String value) {
        if (!Utils.areEqual(this.monthViewClass, value)) {
            this.monthViewClass = value;
            this.updateAdapterViewConfig();
        }
    }

    public final DayBinder getDayBinder() {
        return this.dayBinder;
    }

    /**
     * setDayBinder
     *
     * @param value int
     */
    public final void setDayBinder(DayBinder<?> value) {
        this.dayBinder = value;
        this.invalidateViewHolders();
    }

    /**
     * getMonthHeaderBinder
     *
     * @return MonthHeaderFooterBinder
     */
    public final MonthHeaderFooterBinder getMonthHeaderBinder() {
        return this.monthHeaderBinder;
    }

    /**
     * setMonthHeaderBinder
     *
     * @param value MonthHeaderFooterBinder
     */
    public final void setMonthHeaderBinder(MonthHeaderFooterBinder<?> value) {
        this.monthHeaderBinder = value;
        this.invalidateViewHolders();
    }

    public final MonthHeaderFooterBinder getMonthFooterBinder() {
        return this.monthFooterBinder;
    }

    /**
     * setMonthFooterBinder
     *
     * @param value MonthHeaderFooterBinder
     */
    public final void setMonthFooterBinder(MonthHeaderFooterBinder<?> value) {
        this.monthFooterBinder = value;
        this.invalidateViewHolders();
    }

    public final int getDayViewResource() {
        return this.dayViewResource;
    }

    /**
     * 设置自定义view的布局id
     *
     * @param value int
     * @throws IllegalArgumentException
     */
    public final void setDayViewResource(int value) {
        if (this.dayViewResource != value) {
            if (value == 0) {
                throw new IllegalArgumentException("'dayViewResource' attribute not provided.");
            }
            this.dayViewResource = value;
            updateAdapterViewConfig();
        }
    }

    public final int getMonthHeaderResource() {
        return this.monthHeaderResource;
    }

    /**
     * setMonthHeaderResource
     *
     * @param value int
     */
    public final void setMonthHeaderResource(int value) {
        if (this.monthHeaderResource != value) {
            this.monthHeaderResource = value;
            this.updateAdapterViewConfig();
        }
    }

    public final int getMonthFooterResource() {
        return this.monthFooterResource;
    }

    /**
     * setMonthFooterResource
     *
     * @param value monthFooterResource
     */
    public final void setMonthFooterResource(int value) {
        if (this.monthFooterResource != value) {
            this.monthFooterResource = value;
            this.updateAdapterViewConfig();
        }
    }

    private void updateAdapterViewConfig() {
        if (getCalendarAdapter() != null) {
            calendarAdapter.setViewConfig(
                new ViewConfig(dayViewResource, monthHeaderResource, monthFooterResource, monthViewClass));
            invalidateViewHolders();
        }
    }

    private void invalidateViewHolders() {
        // This does not remove visible views.
        // recycledViewPool.clear()

        // This removes all views but is internal.
        // removeAndRecycleViews()
        if (internalConfigUpdate) {
            return;
        }
        if (getCalendarAdapter() == null || calendarLayoutManager == null) {
            return;
        }
        getContext().getUITaskDispatcher().asyncDispatch(() -> {
            calendarAdapter.notifyMonthScrollListenerIfNeeded();
        });
    }

    private void updateAdapterMonthConfig() {
        updateAdapterMonthConfig(null);
    }

    private void updateAdapterMonthConfig(MonthConfig config) {
        if (internalConfigUpdate || startMonth == null || endMonth == null || firstDayOfWeek == null) {
            return;
        }
        if (getCalendarAdapter() != null) {
            if (config == null) {
                config = new MonthConfig(
                    outDateStyle,
                    inDateStyle,
                    maxRowCount,
                    startMonth,
                    endMonth,
                    firstDayOfWeek,
                    hasBoundaries
                );
            }
            calendarAdapter.setMonthConfig(config);
            calendarAdapter.notifyDataChanged();
            getContext().getUITaskDispatcher().asyncDispatch(() -> {
                calendarAdapter.notifyMonthScrollListenerIfNeeded();
            });
        }
    }

    /**
     * Update [inDateStyle], [outDateStyle], [maxRowCount] and [hasBoundaries]
     * without generating the underlying calendar data multiple times.
     * See [updateMonthConfigurationAsync] if you wish to do this asynchronously.
     */
    public void updateMonthConfiguration() {
        updateMonthConfiguration(inDateStyle, outDateStyle, maxRowCount, hasBoundaries);
    }

    /**
     * updateMonthConfiguration
     *
     * @param inDateStyle InDateStyle
     * @param outDateStyle OutDateStyle
     * @param maxRowCount int
     * @param hasBoundaries boolean
     */
    public final void updateMonthConfiguration(InDateStyle inDateStyle, OutDateStyle outDateStyle, int maxRowCount, boolean hasBoundaries) {
        this.internalConfigUpdate = true;
        this.setInDateStyle(inDateStyle);
        this.setOutDateStyle(outDateStyle);
        this.setMaxRowCount(maxRowCount);
        this.setHasBoundaries(hasBoundaries);
        this.internalConfigUpdate = false;
        updateAdapterMonthConfig();
    }

    /**
     * Update [inDateStyle], [outDateStyle], [maxRowCount] and [hasBoundaries]
     * asynchronously without generating the underlying calendar data multiple times.
     * Useful if your [startMonth] and [endMonth] values are many years apart.
     * See [updateMonthConfiguration] if you wish to do this synchronously.
     */
    public void updateMonthConfigurationAsync() {
        updateMonthConfigurationAsync(inDateStyle, outDateStyle, maxRowCount, hasBoundaries);
    }

    /**
     * updateMonthConfigurationAsync
     *
     * @param inDateStyle InDateStyle
     * @param outDateStyle OutDateStyle
     * @param maxRowCount int
     * @param hasBoundaries boolean
     */
    public void updateMonthConfigurationAsync(InDateStyle inDateStyle, OutDateStyle outDateStyle, int maxRowCount, boolean hasBoundaries) {
        this.internalConfigUpdate = true;
        this.setInDateStyle(inDateStyle);
        this.setOutDateStyle(outDateStyle);
        this.setMaxRowCount(maxRowCount);
        this.setHasBoundaries(hasBoundaries);
        this.internalConfigUpdate = false;
        MonthConfig monthConfig = generateMonthConfig();
        getContext().getUITaskDispatcher().asyncDispatch(() -> {
            updateAdapterMonthConfig(monthConfig);
        });
    }

    /**
     * Set the [monthPaddingStart], [monthPaddingTop], [monthPaddingEnd] and [monthPaddingBottom]
     * values without invalidating the view holders multiple times which would happen if these
     * values were set individually.
     */
    public final void setMonthPadding() {
        setMonthPadding(monthPaddingStart, monthPaddingTop, monthPaddingEnd, monthPaddingEnd);
    }

    /**
     * setMonthPadding 设置月份的Padding
     *
     * @param start int
     * @param top int
     * @param end int
     * @param bottom int
     */
    public final void setMonthPadding(int start, int top, int end, int bottom) {
        this.monthPaddingStart = start;
        this.monthPaddingTop = top;
        this.monthPaddingEnd = end;
        this.monthPaddingBottom = bottom;
        this.invalidateViewHolders();
    }

    public final int getMonthPaddingStart() {
        return this.monthPaddingStart;
    }

    public final int getMonthPaddingEnd() {
        return this.monthPaddingEnd;
    }

    public final int getMonthPaddingTop() {
        return this.monthPaddingTop;
    }

    public final int getMonthPaddingBottom() {
        return this.monthPaddingBottom;
    }

    public final int getMonthMarginStart() {
        return this.monthMarginStart;
    }

    public final int getMonthMarginEnd() {
        return this.monthMarginEnd;
    }

    public final int getMonthMarginTop() {
        return this.monthMarginTop;
    }

    public final int getMonthMarginBottom() {
        return this.monthMarginBottom;
    }

    public MonthScrollListener getMonthScrollListener() {
        return monthScrollListener;
    }

    public void setMonthScrollListener(MonthScrollListener monthScrollListener) {
        this.monthScrollListener = monthScrollListener;
    }

    /**
     * Set the [monthMarginStart], [monthMarginTop], [monthMarginEnd] and [monthMarginBottom]
     * values without invalidating the view holders multiple times which would happen if these
     * values were set individually.
     */
    public final void setMonthMargins() {
        setMonthMargins(monthMarginStart, monthMarginTop, monthMarginEnd, monthMarginBottom);
    }

    /**
     * setMonthMargins
     *
     * @param start int
     * @param top int
     * @param end int
     * @param bottom int
     */
    public final void setMonthMargins(int start, int top, int end, int bottom) {
        this.monthMarginStart = start;
        this.monthMarginTop = top;
        this.monthMarginEnd = end;
        this.monthMarginBottom = bottom;
        this.invalidateViewHolders();
    }

    /**
     * Scroll to (a) specific month on the calendar. This only
     * shows the view for the month without any animations.
     * For (a) smooth scrolling effect, use [smoothScrollToMonth]
     *
     * @param month YearMonth
     */
    public void scrollToMonth(YearMonth month) {
        getCalendarLayoutManager().scrollToMonth(month);
    }

    /**
     * smoothScrollToMonth
     *
     * @param month YearMonth
     */
    public final void smoothScrollToMonth(YearMonth month) {
        this.getCalendarLayoutManager().smoothScrollToMonth(month);
    }

    /**
     * scrollToDay
     *
     * @param day CalendarDay
     */
    public final void scrollToDay(CalendarDay day) {
        this.getCalendarLayoutManager().scrollToDay(day);
    }

    /**
     * scrollToDate
     *
     * @param date LocalDate
     */
    public final void scrollToDate(LocalDate date) {
        scrollToDate(date, DayOwner.THIS_MONTH);
    }

    /**
     * scrollToDate
     *
     * @param date LocalDate
     * @param owner DayOwner
     */
    public final void scrollToDate(LocalDate date, DayOwner owner) {
        this.scrollToDay(new CalendarDay(date, owner));
    }

    /**
     * smoothScrollToDay
     *
     * @param day CalendarDay
     */
    public final void smoothScrollToDay(CalendarDay day) {
        this.getCalendarLayoutManager().smoothScrollToDay(day);
    }

    /**
     * LocalDate
     *
     * @param date LocalDate
     */
    public final void smoothScrollToDate(LocalDate date) {
        this.smoothScrollToDate(date, DayOwner.THIS_MONTH);
    }

    /**
     * smoothScrollToDate
     *
     * @param date LocalDate
     * @param owner DayOwner
     */
    public final void smoothScrollToDate(LocalDate date, DayOwner owner) {
        this.smoothScrollToDay(new CalendarDay(date, owner));
    }

    /**
     * Notify the CalendarView to reload the cell for this [CalendarDay]
     * This causes [DayBinder.bind] to be called with the [ViewContainer]
     * at this position. Use this to reload (a) date cell on the Calendar.
     *
     * @param day CalendarDay
     */
    public final void notifyDayChanged(CalendarDay day) {
        this.getCalendarAdapter().reloadDay(day);
    }

    /**
     * Shortcut for [notifyDayChanged] with (a) [LocalDate] instance.
     *
     * @param date LocalDate
     */
    public final void notifyDateChanged(LocalDate date) {
        this.notifyDateChanged(date, DayOwner.THIS_MONTH);
    }

    /**
     * notifyDateChanged
     *
     * @param date LocalDate
     * @param owner DayOwner
     */
    public final void notifyDateChanged(LocalDate date, DayOwner owner) {
        this.notifyDayChanged(new CalendarDay(date, owner));
    }

    /**
     * Notify the CalendarView to reload the view for this [YearMonth]
     * This causes the following sequence pf events:
     * [DayBinder.bind] will be called for all dates in this month.
     * [MonthHeaderFooterBinder.bind] will be called for this month's header view if available.
     * [MonthHeaderFooterBinder.bind] will be called for this month's footer view if available.
     *
     * @param month YearMonth
     */
    public final void notifyMonthChanged(YearMonth month) {
        this.getCalendarAdapter().reloadMonth(month);
    }

    /**
     * Notify the CalendarView to reload all months.
     * Just like calling [notifyMonthChanged] for all months.
     */
    public final void notifyCalendarChanged() {
        this.getCalendarAdapter().reloadCalendar();
    }

    /**
     * Find the first visible month on the CalendarView.
     *
     * @return The first visible month or null if not found.
     */
    public final CalendarMonth findFirstVisibleMonth() {
        return this.getCalendarAdapter().findFirstVisibleMonth();
    }

    /**
     * Find the last visible month on the CalendarView.
     *
     * @return The last visible month or null if not found.
     */
    public final CalendarMonth findLastVisibleMonth() {
        return this.getCalendarAdapter().findLastVisibleMonth();
    }

    /**
     * Find the first visible day on the CalendarView.
     * This is the day at the top-left corner of the calendar.
     *
     * @return The first visible day or null if not found.
     */
    public final CalendarDay findFirstVisibleDay() {
        return this.getCalendarAdapter().findFirstVisibleDay();
    }

    /**
     * Find the last visible day on the CalendarView.
     * This is the day at the bottom-right corner of the calendar.
     *
     * @return The last visible day or null if not found.
     */
    public final CalendarDay findLastVisibleDay() {
        return this.getCalendarAdapter().findLastVisibleDay();
    }

    /**
     * 由于日历天的子view宽高相等，只取决于父控件CalendarView的宽
     * 因此只有当CalendarView的宽改变时再重新测算日历子view宽高并刷新UI
     */
    private int mCalendarViewWith = SQUARE;

    /**
     * 返回当前控件的大小
     * 但是鸿蒙的onEstimateSize在子控件数量变更时都会回调
     *
     * @param i 宽
     * @param i1 高
     * @return boolean
     */
    @Override
    public boolean onEstimateSize(int i, int i1) {
        int width = i;
        int height = i1;
        if (autoSize && mCalendarViewWith != width) {
            mCalendarViewWith = width;
            int widthMode = EstimateSpec.getMode(width);
            int widthSize = EstimateSpec.getSize(width);
            int heightMode = EstimateSpec.getMode(height);
            if (widthMode == EstimateSpec.ESTIMATED_STATE_BIT_MASK && heightMode == EstimateSpec.ESTIMATED_STATE_BIT_MASK) {
                throw new UnsupportedOperationException("Cannot calculate the values for day Width/Height with the current configuration.");
            }
            int mWidth = (int) ((double) ((float) (widthSize - (this.monthPaddingStart + this.monthPaddingEnd)) / 7.0F) + 0.5D);
            int mHeight = this.autoSizeHeight == SQUARE ? mWidth : this.autoSizeHeight;
            Size computedSize = new Size(mWidth, mHeight);
            this.sizedInternally = true;
            this.setDaySize(computedSize);
            this.sizedInternally = false;
            this.invalidateViewHolders();
        }
        return false;
    }

    @Override
    public void onItemAdded(Component component, int position) {
        updateOnItemChange();
    }

    @Override
    public void onItemRemoved(Component component, int position) {
        updateOnItemChange();
    }

    private void updateOnItemChange() {
        getContext().getUITaskDispatcher().asyncDispatch(() -> {
            calendarAdapter.notifyMonthScrollListenerIfNeeded();
        });
    }

    /**
     * 获取是否在滑动
     *
     * @return boolean 返回是否正在滑动
     */
    public boolean isScroll() {
        return scrollHelper != null && !scrollHelper.isFinished();
    }

    @Override
    public void onScrolled() {
        getContext().getUITaskDispatcher().asyncDispatch(() -> {
            // 滚动停止
            if (!isScroll()) {
                if (calendarAdapter != null) {
                    calendarAdapter.notifyMonthScrollListenerIfNeeded();

                    // 判断是否滑动到下一项
                    calendarAdapter.scrollToNextPositionAuto();
                }
            }
        });
    }

    /**
     * MonthRangeDiffCallback
     *
     * @since 2021-02-26
     */
    private static final class MonthRangeDiffCallback extends DiffUtil.Callback {
        private final List oldItems;
        private final List newItems;

        public MonthRangeDiffCallback(List oldItems, List newItems) {
            super();
            this.oldItems = oldItems;
            this.newItems = newItems;
        }

        /**
         * 获取oldItems的数量
         *
         * @return int，oldItems的数量
         */
        public int getOldListSize() {
            return this.oldItems.size();
        }

        /**
         * 获取newItems的数量
         *
         * @return int，newItems的数量
         */
        public int getNewListSize() {
            return this.newItems.size();
        }

        /**
         * 判断两个对象是否相同
         *
         * @param oldItemPosition The position of the item in the old list
         * @param newItemPosition The position of the item in the new list
         * @return boolean 是否相同
         */
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            return Utils.areEqual(this.oldItems.get(oldItemPosition), this.newItems.get(newItemPosition));
        }

        /**
         * 判断两个对象是否相同
         *
         * @param oldItemPosition The position of the item in the old list
         * @param newItemPosition The position of the item in the new list which replaces the
         * oldItem
         *
         * @return boolean 是否相同
         */
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            return this.areItemsTheSame(oldItemPosition, newItemPosition);
        }
    }

    /**
     * sizeAutoWidth
     *
     * @param height 高度
     * @return Size
     */
    public static Size sizeAutoWidth(int height) {
        return new Size(SQUARE, height);
    }

    /**
     * 更新高度到LayoutConfig
     *
     * @param height 高度
     */
    public void updateLayoutConfig(int height) {
        ComponentContainer.LayoutConfig layoutConfig = getLayoutConfig();
        layoutConfig.height = height;
        setLayoutConfig(layoutConfig);
    }

    /**
     * 是否有动画在进行
     */
    private boolean isAnimating = false;

    public boolean isAnimating() {
        return isAnimating;
    }

    public void setAnimating(boolean animating) {
        isAnimating = animating;
    }
}
